package service

import (
	"GopherFocus/app/dao"
	"GopherFocus/app/model"
	"context"
	"github.com/gogf/gf/errors/gerror"
	"github.com/gogf/gf/os/gcache"
	"github.com/gogf/gf/util/gconv"
	"time"
)

var Category = categoryService{}

type categoryService struct{}

const (
	idToCategoryMapCacheKey   = "category_map_cache"
	idToCategoryCacheDuration = time.Hour
	treeCacheKey              = "category_tree_cache"
	treeCacheDuration         = time.Hour
)

// 获取指定栏目ID及其下面所有子ID，构成切片返回
// 返回的结果总包含指定的栏目ID
func (s *categoryService) GetSubIdList(ctx context.Context, id uint) ([]uint, error) {
	m, err := s.GetMap(ctx)
	if err != nil {
		return nil, err
	}
	// 获取id对应的category实例
	entity := m[id]
	if entity == nil {
		return nil, gerror.Newf(`%d栏目不存在`, id)
	}
	// 获取以ID等于0为根节点的所有子树
	tree, err := s.GetTree(ctx, entity.ContentType)
	if err != nil {
		return nil, err
	}
	// GetSubIdListByTree获取以id为根节点的所有子孙节点的id
	// 返回指定id的所有子孙id，返回结果中包含指定的id
	return append([]uint{id}, s.GetSubIdListByTree(id, tree)...), nil
}

// 获取指定ID对应节点的所有子孙节点（不仅是孩子）的ID， 构成slice返回
func (s *categoryService) GetSubIdListByTree(id uint, tree []*model.CategoryTreeItem) []uint {
	idArray := make([]uint, 0)
	for _, item := range tree {
		// 深度优先遍历
		if item.ParentId == id {
			idArray = append(idArray, item.Id)
			if len(item.Items) > 0 {
				// 由于每一个CategoryTreeItem节点都有一个Items属性，该属性包含该节点的所有孩子节点(每一个孩子节点又包含它自身的孩子)
				// 所以需要递归从而获取所有子孙节点的ID
				idArray = append(idArray, s.GetSubIdListByTree(item.Id, item.Items)...)
			}
		} else if len(item.Items) > 0 {
			idArray = append(idArray, s.GetSubIdListByTree(id, item.Items)...)
		}
	}
	return idArray
}

// id为0的节点为根节点，根节点有孩子节点A1，A2.。。。An(每个孩子节点又包含其所有孩子节点)
// 其中类型为contentType的子节点有A1, A3....An-1则返回[&A1, &A3, ... , &An-1]
func (s *categoryService) GetTree(ctx context.Context, contentType string) ([]*model.CategoryTreeItem, error) {
	// 缓存控制
	v, err := gcache.GetOrSetFunc(treeCacheKey+contentType, func() (i interface{}, e error) {
		entities, err := s.GetList(ctx)
		if err != nil {
			return nil, err
		}
		// 以Id为0的节点的所有子节点作为根节点，构建所有子树，然后将子树存入slice中返回
		tree, err := s.formTree(0, contentType, entities)
		if err != nil {
			return nil, err
		}
		return tree, nil
	}, treeCacheDuration)
	if err != nil {
		return nil, err
	}
	return v.([]*model.CategoryTreeItem), nil
}

// 构造树形结构, 获取parentId对应的所有类型为contentType的子节点栏目树
// 例如parentId有三个孩子节点A，B，C；以A，B，C为根节点的树为ATree，BTree，CTree
// 则返回结果为: [&ATree, &BTree, &CTree]
func (s *categoryService) formTree(parentId uint, contentType string, entities []*model.Category) ([]*model.CategoryTreeItem, error) {
	tree := make([]*model.CategoryTreeItem, 0)
	for _, entity := range entities {
		if contentType != "" && entity.ContentType != contentType {
			continue
		}
		if entity.ParentId == parentId {
			subTree, err := s.formTree(entity.Id, contentType, entities)
			if err != nil {
				return nil, err
			}
			subItem := &model.CategoryTreeItem{
				Items: subTree,
			}
			// 将第一个参数转换为第二个参数对应的结构体类型, 第二个参数必须传入一个结构体指针或双重结构体指针
			if err = gconv.Struct(entity, subItem); err != nil {
				return nil, err
			}
			tree = append(tree, subItem)
		}
	}
	return tree, nil
}

// 根据ID获取单个Category
func (s *categoryService) GetItem(ctx context.Context, id uint) (*model.Category, error) {
	// 由于GetMap会事先把所有id到Category的映射关系存储到内存中，所以可以直接从内存取出，比每次都去数据库查询要快
	m, err := s.GetMap(ctx)
	if err != nil {
		return nil, err
	}
	return m[id], nil
}

// 获取所有栏目ID到Category对象的映射关系，并且保存到内存中，避免每次都要去数据库查询
func (s *categoryService) GetMap(ctx context.Context) (map[uint]*model.Category, error) {
	// 如果cache中存在key，则返回key对应的func返回值，否则将key到func的映射存入cache，然后返回func的返回值
	v, err := gcache.GetOrSetFunc(idToCategoryMapCacheKey, func() (i interface{}, err error) {
		entities, err := s.GetList(ctx)
		if err != nil {
			return nil, err
		}
		m := make(map[uint]*model.Category)
		for _, entity := range entities {
			// 此处如果直接m[entity.Id] = entity; 则最后m中的所有value的地址都将一样
			item := entity
			m[entity.Id] = item
		}
		return m, nil
	}, idToCategoryCacheDuration)
	if err != nil {
		return nil, err
	}
	return v.(map[uint]*model.Category), nil
}

// 获取category（栏目）列表
func (s *categoryService) GetList(ctx context.Context) (list []*model.Category, err error) {
	// orderAsc是按升序排序，dao.Category.C表示categoryColumns，dao.Category.C.Sort表示Sort字段
	// Sort字段，数值越低越靠前，默认为添加时的时间戳，可用于置顶
	err = dao.Category.Ctx(ctx).
		OrderAsc(dao.Category.C.Sort).
		OrderAsc(dao.Category.C.Id).
		Scan(&list)
	return
}
